﻿/// <summary>
/// Implements objects to satisfy the condition checks Modifier objects use.
/// </summary>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Reflection;
using SpaceSim.External;

namespace SpaceSim
{
    /// <summary>
    /// Operation specified in "Condition" classes to compare two values.
    /// </summary>
    public enum ConditionOperator
    {
        LessThan,
        LessThanOrEqual,
        Equal,
        NotEqual,
        GreaterThan,
        GreaterThanOrEqual
    }

    /// <summary>
    /// Represents a condition used by ConditionGroup that must be met for a Check function call to
    /// succeed.
    /// </summary>
    [Serializable]
    public class Condition
    {
        public Condition()
        {
            Operator = ConditionOperator.Equal;
        }

        [XmlAttribute()]
        public ConditionOperator Operator;

        [XmlAttribute()]
        public string TargetA;

        [XmlAttribute()]
        public string TargetB;
    }

    /// <summary>
    /// Represents a set of Conditions that a Modifier object must pass before it will apply its
    /// modifications up an object ownership change.
    /// 
    /// To see if it is "passing" its Conditions a Modifier object will call the "Check" method.
    /// </summary>
    [Serializable]
    public class ConditionGroup
    {
        [XmlElement("Condition")]
        public Condition[] Conditions;

        /// <summary>
        /// Checks to see if the Conditions are met.
        /// </summary>
        /// <exception cref="ConditionGroup.Exception">
        /// Thrown if unable to deduce test being specified. Typically means the object types for 
        /// A&B cannot be coerced into string/double, or that you've used an op that isn't 
        /// compatible with the coerced string, likely because your glossary.xml is misconfigured.
        /// </exception>
        /// <param name="ownershipChain"> 
        /// Stack of objects that are the successive parents of this object.
        /// </param>
        /// <returns>Returns true for all Conditions being met, false if any are not.</returns>
        public bool Check(object[] ownershipChain)
        {
            if (Conditions != null)
            {
                foreach (Condition cond in Conditions)
                {
                    object objA = SpaceSim.Misc.ObjectSearch_Get(this, cond.TargetA, ownershipChain);
                    object objB = SpaceSim.Misc.ObjectSearch_Get(this, cond.TargetB, ownershipChain);

                    if (null == objA || null == objB)
                    {
                        continue;
                    }

                    if (!_Check_RunCondition(cond.Operator, objA, objB))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Checks to see if any of the Conditions for this object need to invoke the ownershipChain.
        /// </summary>
        /// <returns>
        /// Returns true if any of the Conditions for this object need to invoke an ownershipChain, 
        /// false otherwise.
        /// </returns>
        public bool CheckIsExternal()
        {
            if (Conditions != null)
            {
                foreach (Condition cond in Conditions)
                {
                    if (Misc.ObjectSearch_IsExternalSearch(cond.TargetA) ||
                        Misc.ObjectSearch_IsExternalSearch(cond.TargetB))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Internal helper method.
        /// 
        /// Runs the actual condition test for caller.
        /// </summary>
        /// <exception cref="ConditionGroup.Exception">
        /// Thrown if unable to deduce test being specified. Typically means the object types for 
        /// A&B cannot be coerced into string/double, or that you've used an op that isn't 
        /// compatible with the coerced string, likely because your glossary.xml is misconfigured.
        /// </exception>
        /// <param name="op">Specifies the condition test to use.</param>
        /// <param name="objA">Left hand side value to test.</param>
        /// <param name="objB">Right hand side value to test.</param>
        /// <returns>Returns true if given condition test succeeds.</returns>
        private bool _Check_RunCondition(ConditionOperator op, object objA, object objB)
        {
            // As number?
            {
                double numberA = 0;
                double numberB = 0;

                if (objA.TryConvert_ToDouble(out numberA) && objB.TryConvert_ToDouble(out numberB))
                {
                    switch (op)
                    {
                        case ConditionOperator.LessThan:            return numberA <  numberB;
                        case ConditionOperator.LessThanOrEqual:     return numberA <= numberB;
                        case ConditionOperator.Equal:               return numberA == numberB;
                        case ConditionOperator.NotEqual:            return numberA != numberB;
                        case ConditionOperator.GreaterThan:         return numberA >  numberB;
                        case ConditionOperator.GreaterThanOrEqual:  return numberA >= numberB;
                    }
                }
            }

            // String?
            {
                string stringA = "";
                string stringB = "";

                if (objA.TryConvert_ToString(out stringA) && objB.TryConvert_ToString(out stringB))
                {
                    switch (op)
                    {
                        case ConditionOperator.Equal:       return  stringA.Equals(stringB, StringComparison.InvariantCultureIgnoreCase);
                        case ConditionOperator.NotEqual:    return !stringA.Equals(stringB, StringComparison.InvariantCultureIgnoreCase);
                    }
                }
            }

            // Okay, I don't understand what the user wants then
            throw new Exception(string.Format("Unable to process condition \"{0}\" with objA=\"{1}\" and objB=\"{2}\"", op, objA, objB));
        }

        /// <summary>
        /// Exception class raised potentially raised by any method on this class.
        /// </summary>
        public class Exception : System.Exception
        {
            public Exception(string message)
                : base(message)
            {
            }
        }
    }
}
